第27關:Permutation!TypeScript 黑板上排列組合:你捨得解開嗎?
Implement permutation type that transforms union types into the array that includes permutations of unions.
實驗一個排列類型 (permutation type),把聯合類型 (union types) 變成一個數組 (array),裡面包含所有可能的排列組合 (permutations)。
type perm = Permutation<'A' | 'B' | 'C'>;
// ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A'
接下來,你的任務是讓下面的type cases測試通過:
type cases = [
Expect<Equal<Permutation<'A'>, ['A']>>,
Expect<Equal<Permutation<'A' | 'B' | 'C'>, ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']>>,
Expect<Equal<Permutation<'B' | 'A' | 'C'>, ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']>>,
Expect<Equal<Permutation<boolean>, [false, true] | [true, false]>>,
Expect<Equal<Permutation<never>, []>>,
]
從以下幾個方向來思考:
聯合類型 (Union Types): 在 TypeScript 中,聯合類型(例如 'A' | 'B' | 'C'
)表示一個值可以是 'A'
、'B'
或 'C'
其中之一。排列的任務就是從聯合類型中提取不同順序的組合。
遞迴解法 (Recursive Approach): 我們需要從聯合類型中每次取出一個元素,然後對剩下的元素進行遞迴排列。
條件型別 (Conditional Types): 條件型別是 TypeScript 用來根據不同條件決定結果類型的功能。在這裡,我們使用它來檢查聯合類型是否已經排列完畢。
Exclude 工具類型 (Exclude Utility Type): Exclude<T, U>
是 TypeScript 的內建工具類型,它可以從聯合類型 T
中排除 U
的部分。例如,Exclude<'A' | 'B' | 'C', 'A'>
會返回 'B' | 'C'
。
解法:
type Permutation<T, K=T> =
[T] extends [never]
? []
: K extends K
? [K, ...Permutation<Exclude<T, K>>]
: never
細節分析:
類型定義 (Type Definition)type Permutation<T, K=T>
:
我們定義了一個 Permutation
的類型,這個類型有兩個參數:T
和 K
。
T
是我們希望生成排列的聯合類型。
K
參數默認為 T
,用來在遞迴過程中代表當前處理的元素。這意味著在初始調用時,K
會與 T
相同,但隨著遞迴的進行,它會逐漸變為 T
中的具體成員。
基礎情況檢查 (Base Case Check)[T] extends [never]
:
這是一個條件型別,用來檢查 T
是否為 never
類型。
為什麼要這樣寫?: 如果 T
為 never
,表示已經沒有可供排列的元素了。此時返回一個空數組 []
作為排列的結束情況,這是遞迴的基礎情況(base case)。這行代碼確保當沒有元素時,排列將停止,並且能正確返回空數組。
遞迴情況 (Recursive Case)K extends K
:
這一行利用 TypeScript 的分配性(distributive)特性。當我們在條件型別中使用 K
時,TypeScript 會自動將 K
的每個成員提取出來進行處理。
為什麼要這樣寫?: 這使得對於聯合類型的每一個成員,我們都可以進行迭代,並且為每個成員生成其排列。
主遞迴邏輯 (Main Recursive Logic)[K, ...Permutation<Exclude<T, K>>]
:
這一行是遞迴的核心邏輯:
K
: 這是當前正在處理的元素,將它放在結果的最前面。...Permutation<Exclude<T, K>>
:
Exclude<T, K>
:這個內建工具類型會從 T
中排除當前的 K
。例如,如果 T
是 'A' | 'B' | 'C'
,且當前 K
是 'A'
,那麼 Exclude<T, K>
將返回 'B' | 'C'
。Permutation<Exclude<T, K>>
:對剩餘的元素進行遞迴排列。這樣,對於每一個選取的 K
,我們都會生成剩下元素的所有排列。...
:展開運算符,將遞迴返回的結果展開並添加到當前 K
前面,形成一個新的元組。兌現無法到達的類型 (Fallback Case): never
:
這是條件型別的回退情況,當上述條件不滿足時返回 never
。在這個上下文中,這行代碼不會被執行,因為前面的條件已經涵蓋了所有可能的情況。
為什麼要這樣寫?: 這一行的存在是為了確保類型系統的一致性,並在未滿足條件時給出明確的錯誤。
額外補充:
關於 分配條件類型 (Distributive Conditional Types)
首先 What is “distributive” ? :
*An operation that produces the same result when applied to an entire expression as when applied individually to each part and then combining the results.
(such as multiplication in a(b + c) = ab + ac)
When conditional types act on a generic type, they become distributive when given a union type. For example, take the following:
type ToArray<Type> = Type extends any ? Type[] : never;
If we plug a union type into ToArray
, then the conditional type will be applied to each member of that union.
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
//^? type StrArrOrNumArr = string[] | number[]
What happens here is that ToArray
distributes on:
string | number;
and maps over each member type of the union, to what is effectively:
ToArray<string> | ToArray<number>;
which leaves us with:
string[] | number[];
Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends
keyword with square brackets.
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'ArrOfStrOrNum' is no longer a union.
type ArrOfStrOrNum = ToArrayNonDist<string | number>;
//^? type ArrOfStrOrNum = (string | number)[]
這樣,我們就能順利通過測試啦 🎉 😭 🎉
本次介紹了 Permutation
的實作,下一關會挑戰 Length of String
,期待再相見!